package com.idega.block.pdf.business;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.faces.component.UIComponent;
import org.jdom.Attribute;
import org.jdom.Content;
import org.jdom.Element;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xml.sax.InputSource;
import com.idega.core.builder.business.BuilderService;
import com.idega.core.business.DefaultSpringBean;
import com.idega.core.file.util.MimeTypeUtil;
import com.idega.event.PDFGeneratedEvent;
import com.idega.graphics.generator.business.PDFGenerator;
import com.idega.idegaweb.IWApplicationContext;
import com.idega.idegaweb.IWMainApplication;
import com.idega.idegaweb.IWResourceBundle;
import com.idega.presentation.IWContext;
import com.idega.presentation.Page;
import com.idega.servlet.filter.IWBundleResourceFilter;
import com.idega.slide.business.IWSlideService;
import com.idega.util.CoreConstants;
import com.idega.util.CoreUtil;
import com.idega.util.IOUtil;
import com.idega.util.ListUtil;
import com.idega.util.StringHandler;
import com.idega.util.StringUtil;
import com.idega.util.xml.XmlUtil;
@Scope(BeanDefinition.SCOPE_SINGLETON)
@Service(PDFGenerator.SPRING_BEAN_NAME_PDF_GENERATOR)
public class PDFGeneratorBean extends DefaultSpringBean implements PDFGenerator {
private static final Logger LOGGER = Logger.getLogger(PDFGeneratorBean.class.getName());
private ITextRenderer renderer = null;
private XMLOutputter outputter = null;
@Autowired
private ApplicationContext applicationContext;
private static final String TAG_DIV = "div";
private static final String ATTRIBUTE_CLASS = "class";
private static final String ATTRIBUTE_STYLE = "style";
private static final String ATTRIBUTE_VALUE_DISPLAY_NONE = "display: none!important;";
public PDFGeneratorBean() {
try {
renderer = new ITextRenderer();
} catch(Exception e) {
LOGGER.log(Level.SEVERE, "Error creating PDF generator!", e);
}
outputter = new XMLOutputter(Format.getPrettyFormat());
}
private boolean generatePDF(IWContext iwc, Document doc, String fileName, String uploadPath) {
return upload(iwc, getPDFBytes(doc), fileName, uploadPath);
}
private synchronized byte[] getPDFBytes(Document doc) {
if (renderer == null || doc == null) {
return null;
}
uploadSourceToRepository(doc);
// Rendering PDF
byte[] memory = null;
ByteArrayOutputStream os = null;
try {
os = new ByteArrayOutputStream();
renderer.setDocument(doc, getHost());
renderer.layout();
renderer.createPDF(os);
memory = os.toByteArray();
} catch(Exception e) {
e.printStackTrace();
return null;
} finally {
IOUtil.close(os);
}
getApplicationContext().publishEvent(new PDFGeneratedEvent(this, doc));
return memory;
}
private boolean upload(IWContext iwc, byte[] memory, String fileName, String uploadPath) {
// Checking result of rendering process
if (memory == null || StringUtil.isEmpty(fileName) || StringUtil.isEmpty(uploadPath)) {
return false;
}
// Checking file name and upload path
if (!fileName.toLowerCase().endsWith(".pdf")) {
fileName += ".pdf";
}
if (!uploadPath.startsWith(CoreConstants.SLASH)) {
uploadPath = CoreConstants.SLASH + uploadPath;
}
if (!uploadPath.endsWith(CoreConstants.SLASH)) {
uploadPath = uploadPath + CoreConstants.SLASH;
}
// Uploading PDF
InputStream is = null;
try {
is = new ByteArrayInputStream(memory);
return getSlideService(iwc).uploadFileAndCreateFoldersFromStringAsRoot(uploadPath, fileName, is, MimeTypeUtil.MIME_TYPE_PDF_1, true);
} catch(Exception e) {
e.printStackTrace();
} finally {
IOUtil.close(is);
}
return false;
}
@Override
public boolean generatePDF(IWContext iwc, UIComponent component, String fileName, String uploadPath, boolean replaceInputs, boolean checkCustomTags) {
Document document = getDocumentToConvertToPDF(iwc, component, replaceInputs, checkCustomTags);
if (document == null) {
return false;
}
return generatePDF(iwc, document, fileName, uploadPath);
}
private void uploadSourceToRepository(Document document) {
if (!getApplication().getSettings().getBoolean("upload_generated_pdf", Boolean.FALSE)) {
return;
}
org.jdom.Document doc = XmlUtil.getJDOMXMLDocument(document);
if (doc == null) {
LOGGER.log(Level.WARNING, "Document is null!");
return;
}
String htmlContent = getBuilderService().getCleanedHtmlContent(outputter.outputString(doc), false, false, true);
if (StringUtil.isEmpty(htmlContent)) {
LOGGER.log(Level.WARNING, "Document converted to HTML is empty!");
return;
}
IWSlideService slide = getSlideService();
if (slide == null) {
return;
}
LOGGER.warning("Uploading HTML code for PDF... Don't do this when CSS for PDF is made!");
try {
slide.uploadFileAndCreateFoldersFromStringAsRoot(CoreConstants.PUBLIC_PATH + CoreConstants.SLASH, "html_for_pdf.html", htmlContent, "text/html", true);
} catch (RemoteException e) {
e.printStackTrace();
}
}
private Document getDocumentToConvertToPDF(IWContext iwc, UIComponent component, boolean replaceInputs, boolean checkCustomTags) {
if (component == null) {
return null;
}
BuilderService builder = getBuilderService();
if (builder == null) {
return null;
}
org.jdom.Document doc = builder.getRenderedComponent(iwc, component, true, false, false);
if (doc == null) {
return null;
}
if (replaceInputs) {
doc = getDocumentWithoutInputs(doc);
}
if (checkCustomTags) {
doc = getDocumentWithModifiedTags(doc);
}
byte[] memory = getDocumentWithFixedMediaType(doc);
if (memory == null) {
return null;
}
Document document = null;
InputStream stream = null;
Reader reader = null;
try {
stream = new ByteArrayInputStream(memory);
reader = new InputStreamReader(stream, CoreConstants.ENCODING_UTF8);
document = XmlUtil.getDocumentBuilder().parse(new InputSource(reader));
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
IOUtil.close(stream);
IOUtil.closeReader(reader);
}
return document;
}
@Override
public boolean generatePDFFromComponent(String componentUUID, String fileName, String uploadPath, boolean replaceInputs, boolean checkCustomTags) {
if (componentUUID == null) {
return false;
}
IWContext iwc = CoreUtil.getIWContext();
if (iwc == null) {
return false;
}
BuilderService builder = getBuilderService(iwc);
if (builder == null) {
return false;
}
UIComponent component = builder.findComponentInPage(iwc, String.valueOf(iwc.getCurrentIBPageID()), componentUUID);
return generatePDF(iwc, component, fileName, uploadPath, replaceInputs, checkCustomTags);
}
@Override
public boolean generatePDFFromPage(String pageUri, String fileName, String uploadPath, boolean replaceInputs, boolean checkCustomTags) {
if (pageUri == null) {
return false;
}
IWContext iwc = CoreUtil.getIWContext();
if (iwc == null) {
return false;
}
BuilderService builder = getBuilderService(iwc);
Page page = null;
try {
page = builder.getPage(builder.getPageKeyByURI(pageUri));
} catch (Exception e) {
e.printStackTrace();
}
return generatePDF(iwc, page, fileName, uploadPath, replaceInputs, checkCustomTags);
}
private byte[] getDocumentWithFixedMediaType(org.jdom.Document document) {
List<Element> styles = getDocumentElements("link", document);
if (!ListUtil.isEmpty(styles)) {
List<Element> headElements = getDocumentElements("head", document);
if (!ListUtil.isEmpty(headElements)) {
Element head = headElements.get(0);
Element inlineStyles = null;
StringBuffer stylesBuffer = null;
List<Element> needless = new ArrayList<Element>();
String mediaAttrName = "media";
String mediaAttrValueAll = "all";
String mediaAttrValuePrint = "print";
String typeAttrName = "type";
String hrefAttrName = "href";
Attribute href = null;
List<String> expectedValues = ListUtil.convertStringArrayToList(new String[] {"text/css"});
for (Element style: styles) {
if (doElementHasAttribute(style, typeAttrName, expectedValues)) {
href = style.getAttribute(hrefAttrName);
String hrefValue = href.getValue();
String cssContent = null;
InputStream streamToContent = null;
try {
if (hrefValue.startsWith(CoreConstants.WEBDAV_SERVLET_URI)) {
streamToContent = getSlideService().getInputStream(hrefValue);
} else if (hrefValue.startsWith("/idegaweb/bundles/")) {
File file = IWBundleResourceFilter.copyResourceFromJarToWebapp(getApplication(), hrefValue);
streamToContent = new FileInputStream(file);
} else {
URL url = new URL(hrefValue);
streamToContent = url.openStream();
}
cssContent = streamToContent == null ? null : StringHandler.getContentFromInputStream(streamToContent);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error getting content from: " + hrefValue, e);
} finally {
IOUtil.close(streamToContent);
}
if (StringUtil.isEmpty(cssContent)) {
setCustomAttribute(style, mediaAttrName, href == null ? mediaAttrValueAll : href.getValue().endsWith("pdf.css") ?
mediaAttrValuePrint : mediaAttrValueAll);
} else {
if (inlineStyles == null) {
inlineStyles = new Element("style");
head.addContent(inlineStyles);
}
if (stylesBuffer == null) {
stylesBuffer = new StringBuffer();
}
stylesBuffer.append("\n/* Style from: ").append(hrefValue).append(" */\n").append(cssContent).append("\n");
needless.add(style);
}
}
}
if (stylesBuffer != null && inlineStyles != null) {
inlineStyles.setText(stylesBuffer.toString());
}
for (Iterator<Element> needlessStyles = needless.iterator(); needlessStyles.hasNext();) {
needlessStyles.next().detach();
}
}
}
String htmlContent = getBuilderService().getCleanedHtmlContent(outputter.outputString(document), false, false, true);
try {
return htmlContent.getBytes(CoreConstants.ENCODING_UTF8);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
private org.jdom.Document getDocumentWithoutInputs(org.jdom.Document document) {
List<Element> needlessElements = new ArrayList<Element>();
// <input>
List<Element> inputs = getDocumentElements("input", document);
if (ListUtil.isEmpty(inputs)) {
return document;
}
String typeAttrName = "type";
String checkedAttrName = "checked";
String className = "replaceForInputStyle";
String valueAttrName = "value";
Attribute valueAttr = null;
String value = null;
boolean needReplace = true;
// Inputs we don't want to be replaced
List<String> typeAttrValues = ListUtil.convertStringArrayToList(new String[] {"button", "image", "password", "reset", "submit"});
List<String> textTypeValue = ListUtil.convertStringArrayToList(new String[] {"text"});
List<String> checkedAttrValues = ListUtil.convertStringArrayToList(new String[] {"checked", Boolean.TRUE.toString(), CoreConstants.EMPTY});
for (Element input: inputs) {
needReplace = !doElementHasAttribute(input, typeAttrName, typeAttrValues);
if (doElementHasAttribute(input, typeAttrName, Arrays.asList("hidden"))) {
needlessElements.add(input);
} else if (needReplace) {
if (doElementHasAttribute(input, typeAttrName, textTypeValue)) {
// Text inputs
valueAttr = input.getAttribute(valueAttrName);
value = valueAttr == null ? null : valueAttr.getValue();
value = StringUtil.isEmpty(value) ? CoreConstants.MINUS : value;
input.setText(value);
input.setName(TAG_DIV);
setCustomAttribute(input, ATTRIBUTE_CLASS, className);
}
else {
// Radio button or check box
if (!doElementHasAttribute(input, checkedAttrName, checkedAttrValues)) {
// We need to hide not selected options
setCustomAttributeToNextElement(input, ATTRIBUTE_STYLE, ATTRIBUTE_VALUE_DISPLAY_NONE);
}
}
}
}
// Removing needless elements
for (Iterator<Element> it = needlessElements.iterator(); it.hasNext();) {
it.next().detach();
}
return document;
}
@SuppressWarnings("unchecked")
private void setCustomAttributeToNextElement(Element element, String attrName, String attrValue) {
if (element == null) {
return;
}
Element parent = element.getParentElement();
if (parent == null) {
return;
}
List<Element> children = parent.getChildren();
if (ListUtil.isEmpty(children)) {
return;
}
Element nextElement = null;
for (Iterator<Element> childrenIter = children.iterator(); (childrenIter.hasNext() && nextElement == null);) {
nextElement = childrenIter.next();
if (nextElement.equals(element) && childrenIter.hasNext()) {
nextElement = childrenIter.next();
}
else {
nextElement = null;
}
}
if (nextElement == null) {
return;
}
setCustomAttribute(nextElement, attrName, attrValue);
}
private org.jdom.Document getDocumentWithModifiedTags(org.jdom.Document document) {
List<String> expectedValues = null;
List<Element> needless = new ArrayList<Element>();
// <div>
List<Element> divs = getDocumentElements(TAG_DIV, document);
if (!ListUtil.isEmpty(divs)) {
expectedValues = ListUtil.convertStringArrayToList(new String[] {"deselected-case"});
List<String> buttonAreaClassValue = ListUtil.convertStringArrayToList(new String[] {"fbc_button_area"});
List<String> errorsClassValue = ListUtil.convertStringArrayToList(new String[] {"xformErrors"});
List<String> displayNoneAttributeValue = ListUtil.convertStringArrayToList(new String[] {ATTRIBUTE_VALUE_DISPLAY_NONE});
for (Element div: divs) {
if (doElementHasAttribute(div, ATTRIBUTE_CLASS, buttonAreaClassValue)) {
needless.add(div);
}
if (doElementHasAttribute(div, ATTRIBUTE_CLASS, errorsClassValue)) {
needless.add(div);
}
if (doElementHasAttribute(div, ATTRIBUTE_STYLE, displayNoneAttributeValue)) {
needless.add(div);
}
}
}
// <legend>
List<Element> legends = getDocumentElements("legend", document);
if (!ListUtil.isEmpty(legends)) {
expectedValues = ListUtil.convertStringArrayToList(new String[] {"label"});
for (Element legend: legends) {
if (doElementHasAttribute(legend, ATTRIBUTE_CLASS, expectedValues)) {
needless.add(legend);
}
}
}
// <span>
List<Element> spans = getDocumentElements("span", document);
if (!ListUtil.isEmpty(spans)) {
expectedValues = ListUtil.convertStringArrayToList(new String[] {"help-text"});
for (Element span: spans) {
if (doElementHasAttribute(span, ATTRIBUTE_CLASS, expectedValues)) {
needless.add(span);
} else if (doElementHasAttribute(span, ATTRIBUTE_CLASS, Arrays.asList("selector-prototype"))) {
needless.add(span);
} else if (doElementHasAttribute(span, ATTRIBUTE_CLASS, Arrays.asList("alert"))) {
needless.add(span);
}
else if (doElementHasAttribute(span, ATTRIBUTE_CLASS, Arrays.asList("required-symbol"))) {
needless.add(span);
}
}
}
// <textarea>
List<Element> textareas = getDocumentElements("textarea", document);
if (!ListUtil.isEmpty(textareas)) {
for (Element textarea: textareas) {
textarea.setName(TAG_DIV);
textarea.setAttribute(new Attribute(ATTRIBUTE_CLASS, "textAreaReplacementForPDFDocument"));
String originalText = textarea.getTextNormalize();
if (StringUtil.isEmpty(originalText)) {
textarea.setText(CoreConstants.MINUS);
} else {
String text = "<div>".concat(originalText).concat("</div>");
while (text.indexOf("src=\"../") != -1) {
text = StringHandler.replace(text, "../", CoreConstants.EMPTY);
}
text = StringHandler.replace(text, "<br data-mce-bogus=\"1\">", CoreConstants.EMPTY);
text = StringHandler.replace(text, "<br mce-bogus=\"1\">", CoreConstants.EMPTY);
text = StringHandler.replace(text, "<br mce_bogus=\"1\">", CoreConstants.EMPTY);
while (text.indexOf("<br>") != -1) {
text = StringHandler.replace(text, "<br>", CoreConstants.EMPTY);
}
org.jdom.Document textAreaContent = XmlUtil.getJDOMXMLDocument(text);
if (textAreaContent != null) {
try {
@SuppressWarnings("unchecked")
List<Content> clonedContent = textAreaContent.cloneContent();
textarea.removeContent();
textarea.setContent(clonedContent);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error setting new content for ex-textarea element: " + text, e);
textarea.setText(originalText);
}
}
}
}
}
// Links
List<Element> links = getDocumentElements("a", document);
if (!ListUtil.isEmpty(links)) {
for (Element link: links) {
if (doElementHasAttribute(link, "name", Arrays.asList("chibaform-head")) && ListUtil.isEmpty(link.getChildren())) {
needless.add(link);
} else if (doElementHasAttribute(link, ATTRIBUTE_CLASS, Arrays.asList("help-icon"))) {
needless.add(link);
}
}
}
// Scripts
List<Element> scripts = getDocumentElements("script", document);
if (!ListUtil.isEmpty(scripts)) {
needless.addAll(scripts);
}
// <iframes>
List<Element> frames = getDocumentElements("iframe", document);
if (!ListUtil.isEmpty(frames)) {
for (Element frame: frames) {
if (ListUtil.isEmpty(frame.getChildren())) {
needless.add(frame);
}
}
}
// <select>
List<Element> selects = getDocumentElements("select", document);
if (!ListUtil.isEmpty(selects)) {
Locale locale = null;
IWContext iwc = CoreUtil.getIWContext();
if (iwc != null) {
locale = iwc.getCurrentLocale();
}
if (locale == null) {
locale = Locale.ENGLISH;
}
IWResourceBundle iwrb = null;
try {
iwrb = IWMainApplication.getDefaultIWMainApplication().getBundle(CoreConstants.CORE_IW_BUNDLE_IDENTIFIER).getResourceBundle(locale);
} catch(Exception e) {
LOGGER.log(Level.WARNING, "Error getting resources bundle by locale: " + locale, e);
}
String defaultLabel = "None of the options selected";
for (Element select: selects) {
if (doElementHasAttribute(select, ATTRIBUTE_CLASS, Arrays.asList("selector-prototype"))) {
needless.add(select);
} else {
Map<String, List<Element>> allOptions = getSelectOptions(select);
if (allOptions != null && !ListUtil.isEmpty(allOptions.values())) {
for (List<Element> options: allOptions.values()) {
// Getting values for selected options
Element option = null;
List<String> selectedOptionsValues = new ArrayList<String>();
for (Iterator<Element> optionsIter = options.iterator(); optionsIter.hasNext();) {
option = optionsIter.next();
if (doElementHasAttribute(option, "selected", Arrays.asList("selected"))) {
selectedOptionsValues.add(option.getTextNormalize());
}
}
if (ListUtil.isEmpty(selectedOptionsValues)) {
selectedOptionsValues.add(iwrb == null ? defaultLabel :
iwrb.getLocalizedString("pdf_generator.none_of_options_selected", defaultLabel));
}
select.setName(TAG_DIV);
select.setAttribute(new Attribute(ATTRIBUTE_CLASS, "selectDropdownReplacementForPDFDocument"));
if (doElementHasAttribute(select, ATTRIBUTE_STYLE, Arrays.asList(ATTRIBUTE_VALUE_DISPLAY_NONE))) {
select.removeAttribute(ATTRIBUTE_STYLE);
}
Element list = new Element("ul");
select.setContent(Arrays.asList(list));
Collection<Element> listItems = new ArrayList<Element>(selectedOptionsValues.size());
for (String value: selectedOptionsValues) {
Element listItem = new Element("li");
listItem.setText(value);
listItems.add(listItem);
}
list.setContent(listItems);
}
}
}
}
}
selects = getDocumentElements("select", document);
if (!ListUtil.isEmpty(selects)) {
// Removing empty selects
needless.addAll(selects);
}
for (Iterator<Element> needlessIter = needless.iterator(); needlessIter.hasNext();) {
needlessIter.next().detach();
}
return document;
}
@SuppressWarnings("unchecked")
private Map<String, List<Element>> getSelectOptions(Element select) {
List<Element> options = null;
List<Element> optionsGroups = select.getChildren("optgroup");
if (ListUtil.isEmpty(optionsGroups)) {
options = select.getChildren("option");
if (ListUtil.isEmpty(options)) {
return null;
}
Map<String, List<Element>> allOptions = new HashMap<String, List<Element>>();
allOptions.put("allOptions", options);
return allOptions;
}
int index = 0;
Map<String, List<Element>> groupedOptions = new HashMap<String, List<Element>>();
for (Element optionsGroup: optionsGroups) {
options = optionsGroup.getChildren("option");
if (!ListUtil.isEmpty(options)) {
groupedOptions.put(String.valueOf(index), options);
index++;
}
}
return groupedOptions;
}
private boolean doElementHasAttribute(Element e, String attrName, List<String> expectedValues) {
if (e == null || attrName == null || expectedValues == null) {
return false;
}
Attribute a = e.getAttribute(attrName);
if (a == null) {
return false;
}
String attrValue = a.getValue();
if (attrValue == null) {
return false;
}
if (expectedValues.contains(attrValue)) {
return true;
}
for (String expectedValue: expectedValues) {
if (attrValue.indexOf(expectedValue) != -1) {
return true;
}
}
return false;
}
private void setCustomAttribute(Element e, String attrName, String attrValue) {
if (e == null || attrName == null || attrValue == null) {
return;
}
Attribute a = e.getAttribute(attrName);
if (a == null) {
a = new Attribute(attrName, attrValue);
e.setAttribute(a);
}
else {
a.setValue(attrValue);
}
}
private List<Element> getDocumentElements(String tagName, org.jdom.Document document) {
List<Element> elements = XmlUtil.getElementsByXPath(document, tagName, XmlUtil.XHTML_NAMESPACE_ID);
return ListUtil.isEmpty(elements) ? new ArrayList<Element>(0) : elements;
}
@Override
public InputStream getStreamToGeneratedPDF(IWContext iwc, UIComponent component, boolean replaceInputs, boolean checkCustomTags) {
return new ByteArrayInputStream(getBytesOfGeneratedPDF(iwc, component, replaceInputs, checkCustomTags));
}
public ApplicationContext getApplicationContext() {
return applicationContext;
}
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
private IWSlideService getSlideService() {
return getServiceInstance(IWSlideService.class);
}
private IWSlideService getSlideService(IWApplicationContext iwac) {
return getServiceInstance(iwac, IWSlideService.class);
}
private BuilderService getBuilderService(IWApplicationContext iwac) {
return getServiceInstance(iwac, BuilderService.class);
}
private BuilderService getBuilderService() {
return getServiceInstance(BuilderService.class);
}
@Override
public byte[] getBytesOfGeneratedPDF(IWContext iwc, UIComponent component, boolean replaceInputs, boolean checkCustomTags) {
return getPDFBytes(getDocumentToConvertToPDF(iwc, component, replaceInputs, checkCustomTags));
}
}